#pragma once

#include <vector>
#include <memory>

#ifdef PUNCH_MSGPACK
#include <msgpack.hpp>
#endif

#include "../BaseBook.h"
#include "../BaseTracker.h"
#include "quote.h"

namespace mdtx::punch::smatch
{
    /**
     * @brief Simple matching engine
     *
     * @tparam QT quote type
     */
    template <class QT>
    class Engine
    {
    public:
        using order_type = BaseOrder<QT>;
        using id_type = typename QT::id_type;
        using quant_type = typename QT::quant_type;

        using book_type = BaseBook<QT>;
        using book_ptr = std::shared_ptr<book_type>;
        using bid_queue_type = typename book_type::bid_queue_type;
        using ask_queue_type = typename book_type::ask_queue_type;

        using tracker_type = BaseTracker<QT>;
        using tracker_ptr = std::shared_ptr<tracker_type>;

    private:
        id_type ins;
        book_ptr book;
        tracker_ptr tracker;

        std::vector<bool> aon;
        std::vector<QT> aon_pop;

    public:
        /**
         * @brief Construct a new Engine object
         *
         * @param ins_id instrument id, the idea is that we create ONE engine per instrument, however book and tracker can be shared across engines
         * @param book book
         * @param tracker tracker
         */
        Engine(id_type ins_id, book_ptr book, tracker_ptr tracker)
            : ins(ins_id),
              book(book),
              tracker(tracker)
        {
        }

        /**
         * @brief Submit an order
         *
         * @param order new order
         */
        void submit(const order_type &order)
        {
            // register order to book
            auto &new_order = book->new_order(ins, order);

            // match new order
            aon.push_back(new_order.is_aon());
            if (new_order.is_limit())
            {
                if (new_order.is_buy())
                {
                    match<true>(new_order, book->ask(ins));
                }
                else
                {
                    match<true>(new_order, book->bid(ins));
                }
            }
            else
            {
                if (order.is_buy())
                {
                    match<false>(new_order, book->ask(ins));
                }
                else
                {
                    match<false>(new_order, book->bid(ins));
                }
            }
            // orderbook callback
            book->on_order(ins, new_order);
        }

#ifdef PUNCH_MSGPACK
        void submit(const char *buf, size_t len)
        {
            order_type order = msgpack::unpack(buf, len).get().as<order_type>();
            if (order.is_cancel())
            {
                cancel(order.seq_id, order.quant);
            }
            else
            {
                submit(order);
            }
        }
#endif

        /**
         * @brief Cancel an order
         *
         * @param seq_id sequence id
         * @param quant quantity to cancel, 0 to cancel all
         */
        void cancel(id_type seq_id, quant_type quant)
        {
            // basic sanity check
            if (seq_id >= aon.size())
            {
                return;
            }

            auto &order = book->order(ins, seq_id);
            if (order.is_cancel() || order.is_fill())
            {
                return;
            }

            if (quant == 0 || quant >= order.quant)
            {
                // cancel all, erase from queue
                if (order.is_buy())
                {
                    book->bid(ins).erase(order);
                }
                else
                {
                    book->ask(ins).erase(order);
                }
                cancel_order(order);
            }
            else
            {
                // cancel part, maintain queued quote quant
                order.quant -= quant;
                if (order.is_buy())
                {
                    book->bid(ins).find(order)->quant = order.quant;
                }
                else
                {
                    book->ask(ins).find(order)->quant = order.quant;
                }
                tracker->on_cancel(order, quant);
            }
            // orderbook callback
            book->on_order(ins, order);
        }

    private:
        /**
         * @brief Cancel an order
         *
         * @param order order
         */
        void cancel_order(order_type &order)
        {
            quant_type quant = order.quant;
            order.quant = 0;
            order.set_cancel();
            tracker->on_cancel(order, quant);
        }

        /**
         * @brief Match an order
         *
         * @tparam lmt limit order
         * @tparam Queue queue type
         * @param order order
         * @param queue offer queue
         */
        template <bool lmt, class Queue>
        void match(order_type &order, Queue &queue)
        {
            if (!order.is_aon() || check_aon<lmt>(order, queue))
            {
                aon_pop.clear();
                while (order.quant > 0 && !queue.empty())
                {
                    const auto &bbo = queue.front();
                    if constexpr (lmt)
                    {
                        /**
                         * @details the logic of using better_price() may seem little bit weird.
                         * Here we treat order.price as same side of BBO, and if order.price is
                         * better, there won't be a match thus break.
                         *
                         */
                        if (Queue::value_compare::better_price(order.price, bbo.price))
                        {
                            break;
                        }
                    }
                    if (aon[bbo.seq_id] && bbo.quant > order.quant)
                    {
                        // BBO is AON and can't be filled, pop to temp list
                        aon_pop.push_back(bbo);
                        queue.pop_front();
                    }
                    else
                    {
                        exec_order(order, queue);
                    }
                }
                // Push back popped AON list
                for (auto it = aon_pop.rbegin(); it != aon_pop.rend(); ++it)
                {
                    queue.push_front(*it);
                }
            }
            if constexpr (lmt)
            {
                if (order.quant > 0)
                {
                    if (order.is_ioc())
                    {
                        // kill remaining IOC order
                        cancel_order(order);
                    }
                    else
                    {
                        // queue remaining order
                        order.set_queue();
                        queue.insert(order);
                        tracker->on_queue(order);
                    }
                }
                else
                {
                    // in case of BBO.quant == order.quant, the order is only set_match()
                    order.set_fill();
                }
            }
            else
            {
                if (order.quant > 0)
                {
                    // kill remaining market order
                    cancel_order(order);
                }
            }
        }

        /**
         * @brief Check if AON order can be executed
         *
         * @tparam lmt limit order
         * @tparam Queue queue type
         * @param order AON order
         * @param queue offer queue
         * @return true order can be executed
         * @return false order can't be executed
         */
        template <bool lmt, class Queue>
        bool check_aon(order_type &order, Queue queue)
        {
            quant_type remain = order.quant;
            for (auto it = queue.begin(); it != queue.end(); ++it)
            {
                if constexpr (lmt)
                {
                    // we only need to compare price when order is limit
                    if (remain <= 0 || Queue::value_compare::better_price(order.price, it->price))
                    {
                        break;
                    }
                }
                else
                {
                    // market
                    if (remain <= 0)
                    {
                        break;
                    }
                }
                if (!aon[it->seq_id] || remain >= it->quant)
                {
                    remain -= it->quant;
                }
            }
            return remain <= 0;
        }

        /**
         * @brief execute an order, only BBO is executed
         *
         * @tparam Queue queue type
         * @param order aggressor order
         * @param queue offer queue
         */
        template <class Queue>
        void exec_order(order_type &order, Queue &queue)
        {
            quant_type price, quant, tnvr;
            auto &bbo = queue.front();
            auto &resting = book->order(ins, bbo.seq_id);
            // price
            price = bbo.price;
            if (resting.quant > order.quant)
            {
                // quant
                quant = order.quant;
                // order
                order.quant = 0;
                order.set_fill();
                // resting
                resting.quant -= quant;
                resting.set_match();
                // queue
                bbo.quant = resting.quant;
            }
            else
            {
                // quant
                quant = resting.quant;
                // order
                order.quant -= quant;
                order.set_match();
                // resting
                resting.quant = 0;
                resting.set_fill();
                // queue
                queue.pop_front();
            }
            // tnvr
            tnvr = price * quant;
            // tracker callback
            tracker->on_match(order, price, quant, tnvr, true);
            tracker->on_match(resting, price, quant, tnvr, false);
            // orderbook callback, only resting is notified, aggressor is notified on submit() level
            book->on_order(ins, resting);
        }
    };
}
